/**
 *  Copyright 2010 Kyle O'Connor, Justin Arling, Mike Mercieca
 *
 *  This program is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation, either version 3 of the License, or
 *  any later version.
 *  
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *  
 *  You should have received a copy of the GNU General Public License
 *  along with this program.  If not, see <http://www.gnu.org/licenses/>.
 * 
 */
package edu.drexel.sd0910.ece01.aqmon.manager;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Properties;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

import org.apache.log4j.Logger;

import com.sshtools.j2ssh.SftpClient;
import com.sshtools.j2ssh.SshClient;
import com.sshtools.j2ssh.authentication.AuthenticationProtocolState;
import com.sshtools.j2ssh.authentication.PasswordAuthenticationClient;
import com.sshtools.j2ssh.sftp.FileAttributes;
import com.sshtools.j2ssh.sftp.SftpFile;
import com.sshtools.j2ssh.transport.IgnoreHostKeyVerification;

import edu.drexel.sd0910.ece01.aqmon.config.ConfigKeys;
import edu.drexel.sd0910.ece01.aqmon.data.JSONFile;
import edu.drexel.sd0910.ece01.aqmon.gui.MainModel;
import edu.drexel.sd0910.ece01.aqmon.util.OSUtils;
import edu.drexel.sd0910.ece01.aqmon.util.TimeUtils;
import edu.drexel.sd0910.ece01.aqmon.util.run.DelegatingExceptionProofRunnable;

/**
 * A Manager that utilizes the J2SSH library to create an SSH (SFTP) connection
 * to a webserver for uploading files to the server.
 * 
 * @author Kyle O'Connor
 * 
 */
public class FileUploadManager extends Manager {

	private final static Logger log = Logger.getLogger(FileUploadManager.class);
	
	private final static String WEB_ROOT = "www";
	private final static String DEPLOYMENTS_ROOT = "deployments";
	private final static String TEMPLATE_FILENAME = "template.htm";
	private final static String INDEX_FILENAME = "index.htm";
	public final static  String NEW_DEPLOYMENT_NAME_TYPE = "Enter a new name...";

	private SshClient ssh;
	private SftpClient sftp;
	private String hostname;

	/**
	 * Creates a <code>FileUploadManager</code> to control the SSH connection and file
	 * uploading.
	 * 
	 * @param mainModel the data model
	 * @param props properties from data model
	 */
	public FileUploadManager(MainModel mainModel, Properties props) {
		super(mainModel, props);
		this.hostname = props.getProperty(ConfigKeys.HOSTNAME_PROP);
		ssh = new SshClient();

		// create a new scheduled thread pool executor with 2 threads
		super.setExecutorType(NEW_SCHEDULED_THREAD_POOL);
	}

	@Override
	public void startAllTasks() {
		setupUploadDirectory();
		uploadNodeLocations();
		log.debug("starting FileUploadManager tasks");

		// start exception proof connection monitor task every 30 seconds
		((ScheduledExecutorService) getExecutorService()).scheduleWithFixedDelay(new DelegatingExceptionProofRunnable(
				new ConnectionMonitorTask()), TimeUtils.THIRTY_SEC_IN_MS, TimeUtils.THIRTY_SEC_IN_MS,
				TimeUnit.MILLISECONDS);

		// create and schedule an exception proof task that periodically uploads
		// the datastore to the web server
		long period = Long.parseLong(props.getProperty(ConfigKeys.UPLOAD_FREQ_PROP, "900000"));
		((ScheduledExecutorService) getExecutorService()).scheduleWithFixedDelay(new DelegatingExceptionProofRunnable(
				new DataUploadTask()), TimeUtils.FIVE_MIN_IN_MS, period, TimeUnit.MILLISECONDS);
	}

	@Override
	public void stopAllTasks() {
		log.debug("canceling FileUploadManager tasks");
		stopExecutorService();
	}
	
	private void setupUploadDirectory() {
		log.debug("Setting up upload directory");
		String dir = model.getDeploymentDirectory();
		try {
			if (doesFolderExist(dir)) {
				// folder already exists
				sftp.rm(dir, true, true); // force the recursive deletion of existing folder
			}
			sftp.mkdir(dir); // create it
			sftp.cd(dir); // switch to the folder so everything is uploaded here
			log.info("Current working dir: " + sftp.pwd());
			// upload template file and rename to index
			sftp.put(OSUtils.getCurrentWorkingDirectory() + TEMPLATE_FILENAME, INDEX_FILENAME);
		} catch (IOException e) {
			log.error(e.toString());
		}
	}

	private void uploadNodeLocations() {
		log.debug("Uploading node locations");
		JSONFile nodeLocations = model.getXbeeManager().exportNodeLocations();
		// boolean alreadyExists =
		// model.getFileUploadManager().doesFileExist(nodeLocations.getFilename());
		try {
			model.getFileUploadManager().uploadFile(nodeLocations);
		} catch (IOException e) {
			log.error("Error uploading file: " + nodeLocations.getFilename());
		}
	}

	public void connect() throws IOException {
		if (!isConnected()) {
			String tempStatus = model.getStatusText();
			model.setStatusText("Connecting to SSH Server...");
			log.debug("Attempting to connect to server");
			// connect to any host (fixes bug that prompts user to accept server key)
			ssh.connect(hostname, new IgnoreHostKeyVerification());
			PasswordAuthenticationClient pwd = new PasswordAuthenticationClient();
			pwd.setUsername(props.getProperty(ConfigKeys.SSH_USERNAME));
			pwd.setPassword(props.getProperty(ConfigKeys.SSH_PASSWORD));
			// Authenticate the user
			int result = ssh.authenticate(pwd);
			if (!(result == AuthenticationProtocolState.COMPLETE)) {
				throw new IOException("Could not authenticate");
			}
			this.sftp = ssh.openSftpClient();
			sftp.cd(WEB_ROOT); // switch to www folder
			sftp.cd(DEPLOYMENTS_ROOT); // switch to deployments folder
			log.debug("Connected to server successfully");
			model.appendToTerminal("(" + new Date() + "): Connected to server successfully.");
			model.setStatusText(tempStatus);
			log.info("Current working dir: " + sftp.pwd());
		} else {
			log.info("Already connected to server");
		}
	}

	public void reconnect() throws IOException {
		this.connect();
		sftp.cd(model.getDeploymentDirectory());
		log.info("Current working dir: " + sftp.pwd());
	}

	public boolean isConnected() {
		return ssh.isConnected();
	}

	public boolean doesFolderExist(String filename) {
		FileAttributes fa;
		try {
			fa = sftp.stat(filename);
			if (fa.isDirectory()) {
				return true;
			}
		} catch (IOException e) {
			return false;
		}
		return false;
	}

	/**
	 * Returns a list of all directories in the current working directory.
	 * 
	 * @return a <code>List</code> of the names of all directories.
	 * @throws IOException
	 *             if an IO error occurs
	 */
	@SuppressWarnings("unchecked")
	public List<String> listDirectories() throws IOException {
		ArrayList<String> dirList = new ArrayList<String>();
		List<SftpFile> ls = sftp.ls();
		for (SftpFile file : ls) {
			if (file.isDirectory()) {
				if (file.getFilename().equals(".")) {
					// do not add this type of dir to the list
					continue;
				} else if (file.getFilename().equals("..")) {
					// do not add this type of dir to the list
					continue;
				} else {
					// add valid directory name to list
					dirList.add(file.getFilename());
				}
			}
		}
		return dirList;
	}

	public void uploadFile(String localFileName, String remoteFileName) throws IOException {
		// log.info("Stat file "+remoteFileName+": "+sftp.stat(remoteFileName));
		sftp.put(localFileName, remoteFileName);
	}

	public void uploadFile(InputStream is, String remoteFileName) throws IOException {
		// log.info("Stat file "+remoteFileName+": "+sftp.stat(remoteFileName));
		sftp.put(is, remoteFileName);
	}

	public void uploadFile(JSONFile jsonFile) throws IOException {
		this.uploadFile(jsonFile.getInputStream(), jsonFile.getFilename());
	}

	public String downloadFile(JSONFile jsonFile) throws IOException {
		ByteArrayOutputStream baos = new ByteArrayOutputStream();
		sftp.get(jsonFile.getFilename(), baos);
		return baos.toString();
	}

	private void disconnect() throws IOException {
		log.debug("Disconnecting from server...");
		if (isConnected()) {
			if (sftp != null) {
				sftp.quit();
			}
			ssh.disconnect();
			log.debug("Disconnected from server successfully");
		} else {
			log.info("Connection already disconnected");
		}
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see edu.drexel.sd0910.ece01.aqmon.manager.Manager#shutdown()
	 */
	@Override
	public void shutdown() throws Exception {
		this.disconnect();
		stopExecutorService();
	}

	/**
	 * A <code>Runnable</code> task that uploads data to the webserver.
	 * 
	 * @author Kyle O'Connor
	 */
	private class DataUploadTask implements Runnable {

		/*
		 * (non-Javadoc)
		 * 
		 * @see java.lang.Runnable#run()
		 */
		@Override
		public void run() {
			log.debug("Upload data task started");
			boolean success = true;

			List<JSONFile> fileData = model.getDataStore().exportJSONData();
			for (JSONFile jf : fileData) {
				try {
					model.getFileUploadManager().uploadFile(jf);
				} catch (IOException e) {
					success = false;
					log.error("Error uploading file: " + jf.getFilename());
					log.error(e);
				}
			}
			
			try {
				model.getFileUploadManager().uploadFile(
						OSUtils.getCurrentWorkingDirectory() + FileOutputManager.OUTPUT_FILENAME,
						FileOutputManager.OUTPUT_FILENAME);
			} catch (IOException e) {
				success = false;
				log.error("Error uploading file: " + FileOutputManager.OUTPUT_FILENAME);
				log.error(e);
			}

			if (success) {
				model.appendToTerminal("(" + new Date() + "): Uploaded data to Web Server.");
			}
		}
	}

	/**
	 * A <code>Runnable</code> task that monitors the connection to the
	 * webserver.
	 * 
	 * @author Kyle O'Connor
	 */
	private class ConnectionMonitorTask implements Runnable {

		/*
		 * (non-Javadoc)
		 * 
		 * @see java.lang.Runnable#run()
		 */
		@Override
		public void run() {
			log.debug("Monitoring SSH connection...");
			if (!model.getFileUploadManager().isConnected()) {
				log.error("Connection to server lost.");
				model.appendToTerminal("(" + new Date() + "): Warning: connection to server lost.");
				try {
					model.getFileUploadManager().reconnect(); // reconnect
				} catch (Exception e) {
					log.error(e.toString());
					log.info("Could not reconnect to server, sleeping for 30 seconds and try again.");
				}
			}
		}
	}

}
